package database;

import java.io.File;
import java.io.IOException;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;

import database.util.FileUtil;

/**
 * Represents a File in which data records will be stored.
 * the data records need to be stored with some order so that it would be possible to 
 * traverse all the data records in the file visiting each one exactly once.
 * @author sravanku
 *
 */
public class DataFile implements Serializable {
	/**
	 * Initial version 1.0
	 */
	private static final long serialVersionUID = -7755265007191437428L;
	private int MAX_REC_ID = 0;
	/*
	 * Deleted records will have the additional <key,value> are added to map.
	 */
	public String fileName;
	public static final String __IS_DELETED__ = "__IS_DELETED__";
	public static final String __IS_DELETED__VAL = "_ITS_DELETED_";

	public final String __RECORD_ID__ = "__RECORD_ID__";
	private Map<Integer,Map<String,String>> records;
	private Map<String, Integer> schema;

	private int no_of_records = 0;


	private Map<String, Index> indexMap;
	public Map<String, String> columnIndexMap;
	private ArrayList<String> columnOrder;
	/**
	 * Default constructor, init() stage
	 */
	private DataFile(){
		records = new HashMap<Integer, Map<String,String>>(); 
		indexMap = new HashMap<String, Index>();
		columnIndexMap = new HashMap<String, String>();
	}

	/**
	 * Load the schema, and the tableName.
	 * @param fileName
	 * @param schema
	 */
	public DataFile(String fileName, Map<String, Integer> schema){
		this();
		this.schema = schema;
		this.fileName = fileName;
	}

	public DataFile(String fileName, Map<String, Integer> schema, ArrayList<String> columnOrder){
		this();
		this.schema = schema;
		this.fileName = fileName;
		this.columnOrder = columnOrder;
	}

	/**
	 * C:	creates and returns a B+tree index on the file over the specified column,
	 * C:	indexName is used to identify the index if it was to be stored on disk and then retrieved from there, 
	 * C:	indexName is unique within the file.
	 * @param indexName
	 * @param column
	 * @return
	 * @throws IllegalArgumentException
	 */
	public Index createIndex(String indexName, String column) throws IllegalArgumentException{
		/*
		 * Basic conditions check pending.
		 */
		if(schema.get(column) == null){
			throw new IllegalArgumentException("Column:" + column + " is not a valid column");
		}
		else if(schema.get(column) > 24){
			throw new IllegalArgumentException("Column length exists 24 (max length allowed for indexed_column)");
		}
		if(indexMap!= null && indexMap.containsKey(indexName)){
			throw new IllegalArgumentException("Index name exists already!");
		}

		Index index = new Index(indexName,records,column,fileName,no_of_records);
		indexMap.put(indexName, index);
		DataManager.indexRefs.put(indexName, fileName);
		columnIndexMap.put(column, indexName);
		return index;
	}

	/**
	 * C:	Returns the map representing the record, or null if key doesn't exist.
	 * @param recordId
	 * @return
	 */
	public Map<String, String> getRecord(int recordId){

		Map<String, String> ret_record = records.get(recordId);
		if(ret_record!= null && ret_record.keySet().contains(__IS_DELETED__)){
			return null;
		}
		return ret_record;
	}

	/**
	 * C:	Insert the data record into the file. 
	 * Also update indexes over the file. 
	 * C:	Returns recordId � internal record identifier(unique)
	 * C:	If the record contains columns that don�t belong to the file or 
	 * C:	the value in any column is longer than the maximum allowed length then throw IllegalArgumentException.
	 * @param record
	 * @return
	 * @throws IllegalArgumentException
	 */
	public int insertRecord(Map<String, String> record) throws IllegalArgumentException {
		Map<String, String> l_record = new HashMap<String, String>();
		// perform validation & insert on success
		for(String col:record.keySet()){
			/*
			 * throw IllegalArgumentException if an invalid column is present in record.
			 */

			Integer schema_col_len = schema.get(col);
			/*
			 * New changes: Input cannot contain __IS_DELETED__, if it has, just ignore
			 */
			if(col.equals(__IS_DELETED__)){
				continue;
			}
			if(schema_col_len == null){
				throw new IllegalArgumentException("Invalid column: "+col);
			}
			if(record.get(col) != null && (record.get(col).length() > schema_col_len)){
				throw new IllegalArgumentException();
			}
			l_record.put(col, record.get(col));
		}

		// temp fix:
		if(record.keySet().contains(__IS_DELETED__)){
			//System.out.println("test_fx");
			record.remove(__IS_DELETED__);
		}

		for(String col:record.keySet()){
			if (columnIndexMap.get(col) != null) {
				indexMap.get(columnIndexMap.get(col)).insertRecordToIndex(record.get(col), MAX_REC_ID);
			}
		}

		records.put(MAX_REC_ID, l_record);
		no_of_records++;
		return MAX_REC_ID++;
	}

	public int newInsertRecord(Map<String, String> input) {
		Map<String,String> record = new HashMap<String, String>();
		for(String col_name:input.keySet()){
			int col_num = Integer.parseInt(col_name);
			String actual_col_name = columnOrder.get(col_num);
			record.put(actual_col_name, input.get(col_name));
		}

		for(String col:record.keySet()){
			if (columnIndexMap.get(col) != null) {
				indexMap.get(columnIndexMap.get(col)).insertRecordToIndex(record.get(col), MAX_REC_ID);
			}
		}

		records.put(MAX_REC_ID,record);
		no_of_records++;
		return MAX_REC_ID++;
	}


	/**
	 * C:	Write file contents to disk, 
	 * C:	if that file was already saved before then overwrite the previous save
	 */
	public void dumpFile(){

		try {
			FileUtil.createFileFromObject(fileName+".file", this);
		} catch (IOException e) {
			System.out.println("Error saving file: DataFile.dumpFile()");
			e.printStackTrace();
		}
		return;
	}
	
	public Map<Integer,Map<String,String>> getRecordsMap(){
		return records;
	}

	/**
	 * C: Print the contents of the file in a human-readable format. 
	 * C: Return as String what was printed. 
	 * C: Ignore deleted records
	 * @return
	 */
	public String viewFile(){
		String output = "";
		for(Integer rec_id:records.keySet()){
			Map<String,String> record = records.get(rec_id);
			if(record != null){ // which is true always.
				String isDeleted = record.get(__IS_DELETED__);
				if(isDeleted != null){
					// record is deleted.
					continue;
				}
				output += rec_id + ":\n";
				for(String col_name:record.keySet()){
					String col_value = record.get(col_name);
					if(col_value == null){
						//don't print this column
						continue;
					}
					output += "\t" + col_name + ": " + col_value + "\n";
				}
			}
		}
		System.out.println(output);
		return output;
	}

	/**
	 * Delete the file, 
	 * Delete all indexes over it and all records inside: 
	 * both in memory and on disk.
	 */
	public void dropFile(){
		/*
		 * To Do --- Remove the index entries in memory(IndexMap).
		 */
		if(indexMap != null){
			for(String index:indexMap.keySet()){
				dropIndex(index);
			}
		}
		indexMap.clear();
		//new File(fileName+".file").delete();
		//return;
	}

	/**
	 * Restore index contents from disk. If index of this name is already over the file or 
	 * index of this name is not stored on the disk throw IllegalArgumentException
	 * @param indexName
	 * @return
	 * @throws IllegalArgumentException
	 */
	public Index restoreIndex(String indexName) throws IllegalArgumentException{
		Index index = new Index(fileName, indexName);
		return index;
	}

	/**
	 * delete index, both in memory and on disk. If index of this name on the file doesn�t exist, do nothing
	 * @param indexName
	 */
	public void dropIndex(String indexName){
		//indexMap.remove(indexName);

		for(String col_name:columnIndexMap.keySet()){
			if(columnIndexMap.get(col_name).equals(indexName)){
				columnIndexMap.remove(col_name);
				break;
			}
		}

		indexMap.get(indexName).deleteIndex(fileName, indexName);
	}

	/**
	 * Return a new FileIterator
	 * @return
	 */
	public Iterator<Integer> iterator(){
		FileIterator fIter = this.new FileIterator();
		return fIter;
	}

	/**
	 * Iterator needs to allow traversal over all records in the file. 
	 * The next() method of Iterator returns recordIds
	 * @author sravanku
	 *
	 */
	private class FileIterator implements Iterator<Integer>{

		/*
		 * Other details:
		 * There may be many iterators for the same file or index at the same time. 
		 * In situation when between iterating steps index or file content is changed by some other iterator or 
		 * method throw ConcurrentModificationException when the next next() or remove() is called.
		 * Record Identifiers should be nonnegative integers.
		 * You will need to add additional methods to above mentioned classes in order for the methods that are needed be able to work 
		 * (e.g. you will need constructors for DataFile and Index).
		 *  Also you may choose to add some other methods, in case you would find them helpful.
		 * (non-Javadoc)
		 * @see java.util.Iterator#hasNext()
		 */
		private int curPos = 0;
		private int lastPos = -1;
		private List<Integer> recordIdList = new ArrayList<Integer>();
		private int iter_no_of_records = no_of_records;

		public FileIterator() {
			for (int recordId :records.keySet()) {
				Map<String, String> record = records.get(recordId);
				if(record.containsKey(__IS_DELETED__) && record.get(__IS_DELETED__).equals(__IS_DELETED__VAL) ){
					continue;
				}
				recordIdList.add(recordId);
			}
			//test
			int k=0;	
		}

		@Override
		public boolean hasNext() {
			return curPos != recordIdList.size() ;
		}

		@Override
		public Integer next() {
			if(isModifiedConcurrently()){
				throw new ConcurrentModificationException("Data modified by another iterator");
			}
			try {
				int next = recordIdList.get(curPos);
				lastPos = curPos;
				curPos++;
				return next;
			} catch(IndexOutOfBoundsException e) {
				throw new NoSuchElementException();
			}
		}

		@Override
		public void remove() {
			if(isModifiedConcurrently()){
				throw new ConcurrentModificationException("Data modified by another iterator");
			}
			if (lastPos < 0)
				throw new IllegalStateException();
			try {
				Map<String, String> record = records.get(recordIdList.get(lastPos));
				record.put(__IS_DELETED__, __IS_DELETED__VAL);
				recordIdList.remove(lastPos);
				iter_no_of_records--;
				no_of_records--;
				if (lastPos < curPos)
					curPos--;
			} catch (IndexOutOfBoundsException e) {
				throw new ConcurrentModificationException();
			}
		}

		public boolean isModifiedConcurrently(){
			if(no_of_records == iter_no_of_records){
				return false;
			}
			return true;
		}

	}

	/**
	 * <b>[OPTIONAL] </b>
	 * Rebuild an existing file by physically removing the data records marked as �deleted.�
	 * 
	 */
	public void rebuildFile(){
		for(Integer rec_id:records.keySet()){
			Map<String,String> record = records.get(rec_id);
			if(record != null){ // which is true always.
				String isDeleted = record.get(__IS_DELETED__);
				if(isDeleted != null){
					// record is deleted.
					records.remove(rec_id);
				}
			}
		}
		dumpFile();		
	}

	/**
	 * <b>[OPTIONAL] </b>
	 * Rebuild an index from a file. If an index of this name on the file doesn�t exist, do nothing
	 * @param indexName
	 * @return Index
	 */
	//The below commented code is building a new index from a file --- The commented code is working
	/*public Index rebuildIndex(String indexName){
		if (indexMap.containsKey(indexName)) {
			String columnName = "";
			for (String colName : columnIndexMap.keySet()) {
				if (columnIndexMap.get(colName).equals(indexName))
					columnName = colName;
					continue;
			}
			Index newIndex = new Index(indexName, records, columnName, fileName,no_of_records);
			indexMap.put(indexName, newIndex);
			return newIndex;
		} else {
			return null;
		}
	}*/

	//The below function is implementing delete function in BPlusTree
	public Index rebuildIndex(String indexName){
		if (indexMap.containsKey(indexName)) {
			String columnName = "";
			for (String colName : columnIndexMap.keySet()) {
				if (columnIndexMap.get(colName).equals(indexName))
					columnName = colName;
				continue;
			}
			Index newIndex = indexMap.get(indexName);

			for (int recordId:records.keySet()) {
				Map<String,String> record = records.get(recordId);
				if (record.containsKey(__IS_DELETED__) && record.get(__IS_DELETED__).equals(__IS_DELETED__VAL) ) {
					newIndex.deleteKeysFromIndex(record.get(columnName));
				} else 
					continue;
			}
			return newIndex;
		} else {
			return null;
		}
	}


}
